#!/usr/bin/perl
use FindBin;
use lib "$FindBin::Bin/lib/perl-lib/lib/perl5";
use lib "$FindBin::Bin/lib";

package HBAConfParser;

use strict;
use POSIX qw(uname strftime);
use IO::File;
use JSON;
use Getopt::Long;

use AutoExecUtils;

sub new {
    my ( $pkg, $filePath, $needBackup ) = @_;

    my $self = {
        filePath    => $filePath,
        needBackup  => $needBackup,
        confItems   => [],
        confItemMap => {},
        headComment => '',
        tailComment => ''
    };

    bless( $self, $pkg );

    if ( defined($filePath) ) {
        if ( not -e $filePath ) {
            $self->{hasError} = 1;
            print("ERROR: $filePath not exists.\n");
        }
        if ( not -f $filePath ) {
            $self->{hasError} = 1;
            print("ERROR: $filePath is not a file.\n");
        }

        my ( $confItems, $confItemMap ) = $self->__read_config( $filePath, 'file' );
        $self->{confItems}   = $confItems;
        $self->{confItemMap} = $confItemMap;
    }

    return $self;
}

sub backup {
    my ( $self, $filePath ) = @_;
    my $dateTimeStr = strftime( "%Y%m%d-%H%M%S", localtime() );
    my $exitCode    = system(qq{cp "$filePath" "$filePath.$dateTimeStr"});
    return $exitCode;
}

sub getFileContent {
    my ( $self, $filePath ) = @_;
    my $content;

    if ( -f $filePath ) {
        my $size = -s $filePath;
        my $fh   = IO::File->new("<$filePath");

        if ( defined($fh) ) {
            $fh->read( $content, $size );
            $fh->close();
        }
        else {
            die("ERROR: File:$filePath open failed, $!.\n");
        }
    }

    return $content;
}

sub __read_config {
    my ( $self, $filePath ) = @_;
    my $content = $self->getFileContent($filePath);
    my ( $confItems, $confItemMap ) = $self->__parseConf($content);
    return ( $confItems, $confItemMap );
}

sub __parseConf {
    my ( $self, $content ) = @_;

    my @confItems   = ();
    my $confItemMap = {};

    my $comment = '';
    $content =~ s/^\s*|\s*$//g;
    foreach my $line ( split( /\n/, $content ) ) {
        $line =~ s/^\s*|\s*$//g;

        # handle comments
        if ( $line =~ /^[;#]/ ) {
            $comment = $comment . $line . "\n";
            next;
        }
        elsif ( $line eq '' ) {
            $comment = $comment . "\n";
            next;
        }

        my @fields = split( /\s+/, $line, 5 );
        my ( $type, $database, $user, $address, $method );
        if ( scalar(@fields) == 5 ) {
            $type     = $fields[0];
            $database = $fields[1];
            $user     = $fields[2];

            $address = $fields[3];
            if ( $address !~ /\/\d+$/ ) {
                $address = '';
                $method  = $fields[3];
            }
            else {
                $method = $fields[4];
            }
        }
        elsif ( scalar(@fields) == 4 ) {
            $type     = $fields[0];
            $database = $fields[1];
            $user     = $fields[2];
            $address  = '';
            $method   = $fields[3];
        }

        if ( defined($type) and defined($database) ) {
            my $item = {
                'TYPE'     => $type,
                'DATABASE' => $database,
                'USER'     => $user,
                'ADDRESS'  => $address,
                'METHOD'   => $method,
                'COMMENT'  => $comment
            };
            my $key = "$type.$database.$user.$address";
            push( @confItems, $item );
            $confItemMap->{$key} = $item;
            $comment = '';
        }
    }

    if ( $comment ne '' ) {
        if ( scalar(@confItems) > 0 ) {
            foreach my $line ( split( /\n/, $comment ) ) {
                if ( index( $self->{tailComment}, $line ) < 0 ) {
                    $self->{tailComment} = $self->{tailComment} . $line . "\n";
                }
            }
        }
        else {
            foreach my $line ( split( /\n/, $comment ) ) {
                if ( index( $self->{headComment}, $line ) < 0 ) {
                    $self->{headComment} = $self->{headComment} . $line . "\n";
                }
            }
        }
    }

    return ( \@confItems, $confItemMap );
}

sub modifyConf {
    my ( $self, $content ) = @_;

    my $confItems   = $self->{confItems};
    my $confItemMap = $self->{confItemMap};
    my ( $newItems, $newConfItemMap ) = $self->__parseConf($content);

    my $modified = 0;
    if ( defined($newItems) ) {
        foreach my $item (@$newItems) {
            my $key     = $item->{TYPE} . '.' . $item->{DATABASE} . '.' . $item->{USER} . '.' . $item->{ADDRESS};
            my $oldItem = $confItemMap->{$key};
            if ( defined($oldItem) ) {
                if ( $oldItem->{METHOD} ne $item->{METHOD} ) {
                    $modified = 1;
                    $oldItem->{METHOD} = $item->{METHOD};
                    print( "INFO: Change " . $item->{TYPE} . "\t" . $item->{DATABASE} . "\t" . $item->{USER} . "\t" . $item->{ADDRESS} . "\t" . $item->{METHOD} . "\n" );
                }
                my $oldComment = $oldItem->{COMMENT};
                my $newComment = $item->{COMMENT};
                foreach my $commentLine ( split( /\n/, $newComment ) ) {
                    if ( index( $oldComment, $commentLine ) < 0 ) {
                        $modified = 1;
                        $oldItem->{COMMENT} = $oldComment . "\n" . $commentLine;
                    }
                }
            }
            else {
                $modified = 1;
                push( @$confItems, $item );
                $confItemMap->{$key} = $item;
                print( "INFO: Append " . $item->{TYPE} . "\t" . $item->{DATABASE} . "\t" . $item->{USER} . "\t" . $item->{ADDRESS} . "\t" . $item->{METHOD} . "\n" );
            }
        }
    }

    my $newContent = '';
    foreach my $item (@$confItems) {
        $newContent = $newContent . $item->{COMMENT};
        my $type     = $item->{TYPE};
        my $database = $item->{DATABASE};
        my $user     = $item->{USER};
        my $address  = $item->{ADDRESS};
        my $method   = $item->{METHOD};

        $type     = $type . ' ' x ( 8 - length($type) );
        $database = $database . ' ' x ( 16 - length($database) );
        $user     = $user . ' ' x ( 16 - length($user) );
        $address  = $address . ' ' x ( 24 - length($address) );

        $newContent = $newContent . "$type $database $user $address $method\n";
    }

    if ( $modified == 1 ) {
        my $filePath = $self->{filePath};
        if ( $self->{needBackup} == 1 ) {
            $self->backup($filePath);
        }

        my $confFile = IO::File->new(">$filePath");
        if ( defined($confFile) ) {
            print $confFile ( $self->{headComment} );
            print $confFile ($newContent);
            print $confFile ( $self->{tailComment} );
            $confFile->close();
        }
        else {
            print("ERROR: Can not write to file $filePath, $!\n");
            $self->{hasError} = 1;
        }
    }
    else {
        print("INFO: Config not changed.\n");
    }

    return $newContent;
}

sub usage {
    my $pname = $FindBin::Script;

    print("$pname --backup 0|1 --filepath <pg_hba.conf directory> --content <hosts content>\n");
    exit(1);
}

sub main {
    $| = 1;    #不对输出进行buffer，便于实时看到输出日志
    my $filePath;
    my $needBackup = 0;
    my $content;

    GetOptions(
        'backup=i'   => \$needBackup,
        'filepath=s' => \$filePath,
        'content=s'  => \$content
    );

    if ( not defined($filePath) or $filePath eq '' ) {
        print("ERROR: Must defined pg_hba.conf path by option --filepath\n");
        usage();
    }

    if ( not defined($content) or $content eq '' ) {
        print("ERROR: Must defined config content by option --content\n");
        usage();
    }

    $content =~ s/\\n/\n/g;

    my $confParser = HBAConfParser->new( $filePath, $needBackup );
    my $hasError   = $confParser->{hasError};
    if ( $hasError == 0 ) {
        my $confContent = $confParser->modifyConf($content);

        my $out = { hbaConf => $confContent };
        AutoExecUtils::saveOutput($out);
        $hasError = $confParser->{hasError};
    }

    return $hasError;
}

exit main();

1;
